﻿#region [ License information          ]
/* ************************************************************
 *
 * Copyright (c) Bjørn Bouet Smith, 2010, 2011
 *
 * This source code is subject to terms and conditions of 
 * Microsoft Public License (Ms-PL).
 * 
 * A copy of the license can be found in the license.txt
 * file at the root of this distribution. If you can not 
 * locate the License, please send an email to bjornsmith@gmail.com
 * 
 * By using this source code in any fashion, you are 
 * agreeing to be bound by the terms of the Microsoft 
 * Permissive License.
 *
 * You must not remove this notice, or any other, from this
 * software.
 *
 * ************************************************************/
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Smith.AMC.Network
{
    /// <summary>
    /// Represents a memcached server node, that is part of the mecached server cluster.
    /// </summary>
    [DebuggerDisplay("IPAddress:{ServerAddress}, PortNo:{PortNumber}, ID:{ServerID}")]
    public class MemcachedServerNode : IMemcachedServerNode
    {
        /// <summary>
        /// The type of socket this node is using for connections to the server
        /// </summary>
        private readonly SocketType socketType;

        /// <summary>
        /// The type of protocol this node is using for connections to the server
        /// </summary>
        private readonly ProtocolType protocol;

        /// <summary>
        /// The maximum number of sockets this node will connect to the server
        /// </summary>
        private readonly int maxSocketsPerServer;

        private List<IClientSocket> allServers;

        /// <summary>
        /// Initializes a new instance of the <see cref="MemcachedServerNode"/> class.
        /// </summary>
        /// <param name="serverAddress">The server address this memcached server node.</param>
        /// <param name="portNumber">The port number to connect to.</param>
        /// <param name="socketType">The type of socket, i.e. stream, datagram.</param>
        /// <param name="protocol">The protocol, i.e. UDP, TCP.</param>
        /// <param name="maxSocketsPerServer">The maximum number of sockets to open against this server node.</param>
        /// <param name="serverID">The server ID.</param>
        public MemcachedServerNode(
            IPAddress serverAddress, 
            int portNumber,
            SocketType socketType,
            ProtocolType protocol, 
            int maxSocketsPerServer, 
            int serverID)
        {
            ServerAddress = serverAddress;
            ServerID = serverID;
            PortNumber = portNumber;
            this.socketType = socketType;
            this.protocol = protocol;
            this.maxSocketsPerServer = maxSocketsPerServer;
            WaitQueue = new SemaphoreSlim(maxSocketsPerServer);
            InitializeSocketQueue();
            HealthCheckSocket = CreateClientSocket();
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is dead.
        /// To be used to remove a server node from the server pool.
        /// </summary>
        /// <value><c>true</c> if this instance is dead; otherwise, <c>false</c>.</value>
        public bool IsDead
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the failed count, i.e. the number of times
        /// a socket have been reported as dead for this server node.
        /// 
        /// This is being used to control whether or not the node should be marked as dead
        /// </summary>
        /// <value>The failed count.</value>
        public int FailedCount
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the server address this server node is listening on.
        /// </summary>
        public IPAddress ServerAddress
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets the server ID.
        /// </summary>
        public int ServerID
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the port number this server node is listening on
        /// </summary>
        /// <value>The port number.</value>
        public int PortNumber
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets the health check socket that is being used to determine whether or not this instance is still alive
        /// when too many clients have reported errors on the memcached node.
        /// </summary>
        /// <value>The health check socket.</value>
        public IClientSocket HealthCheckSocket
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets the socket pool, from which this instance gives out ClientSockets from.
        /// </summary>
        private SocketQueue SocketPool
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the wait queue, where threads will line up while waiting for vacant sockets.
        /// </summary>
        private SemaphoreSlim WaitQueue
        {
            get;
            set;
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            allServers.ForEach(cs => cs.Dispose());
        }

        /// <summary>
        /// Aquires a client socket to be used with operations against this instance of the mecached node.
        /// </summary>
        /// <param name="timeout">The timeout time for obtaining a socket.</param>
        /// <returns>The socket, ready to be used.</returns>
        /// <exception cref="TimeoutException">If the timeout period expired</exception>
        public IClientSocket AquireSocket(TimeSpan timeout)
        {
            IClientSocket socket;

            if (!WaitQueue.Wait(timeout))
            {
                throw new TimeoutException(
                    string.Format("Timeout expired while waiting {0} ms for a free socket", timeout.TotalMilliseconds));
            }

            bool aquired = SocketPool.Dequeue(out socket);
            if (!aquired)
            {
                throw new InvalidOperationException("This should not happen, we have entered the semaphore, but too few sockets was available");
            }

            return socket;
        }

        /// <summary>
        /// Releases the socket back into the socket pool, ready for another thread to pick it up.
        /// </summary>
        /// <param name="socket">The socket to release back.</param>
        public void ReleaseSocket(IClientSocket socket)
        {
            SocketPool.Enqueue(socket);
            WaitQueue.Release();
        }

        /// <summary>
        /// Renews the failed socket by removing the given socket from the queue and recreating it.
        /// Will increment the failed socket count for this server node.
        /// </summary>
        /// <param name="socket">The socket.</param>
        public void RenewFailedSocket(IClientSocket socket)
        {
            // Dispose the socket
            socket.Dispose();

            // Remove from list of all sockets
            allServers.Remove(socket);
            
            // Create a new one and inject it into the queue
            SocketPool.Enqueue(CreateClientSocket());
            
            // Release one into the semaphore, making the socket available.
            WaitQueue.Release();
        }

        /// <summary>
        /// Initializes the socket queue with the required number of sockets available.
        /// The sockets will not be opened, just created
        /// </summary>
        private void InitializeSocketQueue()
        {
            allServers = new List<IClientSocket>();
            for (int x = 0; x < maxSocketsPerServer; x++)
            {
                allServers.Add(CreateClientSocket());
            }

            SocketPool = new SocketQueue(allServers);
        }

        /// <summary>
        /// Creates a client socket to be put into the socket queue
        /// </summary>
        /// <returns>The newly created socket.</returns>
        private IClientSocket CreateClientSocket()
        {
            return new ClientSocket(new IPEndPoint(ServerAddress, PortNumber), socketType, protocol)
                       {
                           OwnerID = ServerID
                       };
        }
    }
}
